package name.cyrax.red.jpv.impl.funcs.asm;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import name.cyrax.red.jpv.api.adapters.DoubleAdapter;
import name.cyrax.red.jpv.api.adapters.IntegerAdapter;
import name.cyrax.red.jpv.api.adapters.LongAdapter;
import name.cyrax.red.jpv.api.adapters.NumericAccessor;
import name.cyrax.red.jpv.api.adapters.NumericAdapter;
import name.cyrax.red.jpv.api.funcs.FunctorInstance;
import name.cyrax.red.jpv.api.funcs.info.FunctorMethod;
import name.cyrax.red.jpv.api.funcs.info.FunctorMethodKind;
import name.cyrax.red.jpv.api.funcs.info.ValueType;
import name.cyrax.red.jpv.impl.adapters.NumericImpl;
import name.cyrax.red.jpv.impl.funcs.asm.CompileMapper.Entry;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

public class AdapterCompiler implements Opcodes {
	
	private final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
	private final List<Object> argsList = new ArrayList<>();
	
	protected ASMContext createContext(CompileMapper.Entry<?> entry)
	{
		ASMContext c = new ASMContext(entry);
		
		FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, 
				c.argName, //"arg0"
				c.argDesc, //"Lname/cyrax/red/jpv/api/adapters/IntegerAdapter;" 
				null, null);
		fv.visitEnd();
		return c;
	}
	
	private Object[] getConstructorArgs() {
		return argsList.toArray(); 
	}
	
	private NumericAdapter createInstance(Class<?> classType) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		Constructor<?> c = classType.getConstructor(Object[].class);
		Object args = getConstructorArgs();
		return (NumericAdapter) c.newInstance(args);
	}
	
	public static int switchOf(ValueType vt, int vInt, int vLong, int vDbl) {
		switch (vt) {
			case INTEGER:
				return vInt;
			case LONG:
				return vLong;
			case DOUBLE:
				return vDbl;
			default:
				throw new IllegalArgumentException("Unsupported type: " + vt);
		}
	}
	
	public static int switchCast(ValueType vtFrom, ValueType vtResult) {
		switch (vtFrom) {
			case INTEGER:
				return switchOf(vtResult, NOP, I2L, I2D);
			case LONG:
				return switchOf(vtResult, L2I, NOP, L2D);
			case DOUBLE:
				return switchOf(vtResult, D2I, D2L, NOP);
			default:
				throw new IllegalArgumentException("Unsupported type: " + vtFrom);
		}
	}
	
	public static NumericAdapter compile(NumericAdapter adapter, String compileClassName) {
		AdapterCompiler compiler = new AdapterCompiler();
		byte[] binary = compiler.compile0(adapter, compileClassName);
		Class<?> dynClass = DynamicClassLoader.getInstance().loadCompiledClass(compileClassName, binary);
		try {
			return compiler.createInstance(dynClass);
		} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
				| IllegalArgumentException | InvocationTargetException e)
		{
			throw new RuntimeException();
		}
	}
	
	private byte[] compile0(NumericAdapter adapter, String compileClassName) {
		String compiledClassName = compileClassName.replace('.', '/');
		final String thisValueType = "L" + compiledClassName + ";";
		
		argsList.clear();
		
		ValueType resultType = adapter.detectType(new NumericAccessor<ValueType>() {

			@Override public ValueType asInteger(IntegerAdapter adapter)	{
				return ValueType.INTEGER;
			}
			@Override public ValueType asLong(LongAdapter adapter)	{
				return ValueType.LONG;
			}
			@Override public ValueType asDouble(DoubleAdapter adapter)	{
				return ValueType.DOUBLE;
			}
		});
		
		final Class<? extends NumericImpl<?>> parentClass;
		switch (resultType) {
			case INTEGER:
				parentClass = IntegerCompileImpl.class;
				break;
			case LONG:
				parentClass = LongCompileImpl.class;
				break;
			case DOUBLE:
				parentClass = DoubleCompileImpl.class;
				break;
			default:
				throw new UnsupportedOperationException("Unsupported result type: " + resultType);
		}
		
		final String parentClassName = Type.getInternalName(parentClass); 

		cw.visit(V1_7, ACC_SUPER + ACC_PUBLIC, compiledClassName, null,
				parentClassName, // "name/cyrax/red/jpv/impl/adapters/IntegerImpl"
				null);
		cw.visitInnerClass(compiledClassName, null, null, 0);
		
		final Map<NumericAdapter, Entry<ASMContext>> mappings = 
			new CompileMapper<ASMContext>(new NumericAdapterVisitor(adapter)) {
				protected ASMContext createCustomInfo(CompileMapper.Entry<?> entry) {
					return createContext(entry);
				};
		}.getMapping();


		MethodVisitor mv;

		{
			mv = cw.visitMethod(ACC_PUBLIC, "<init>", "([Ljava/lang/Object;)V", null, null);
			mv.visitCode();
			Label lFirst = new Label();
			mv.visitLabel(lFirst);
			mv.visitVarInsn(ALOAD, 0);
			mv.visitMethodInsn(INVOKESPECIAL, parentClassName, "<init>", "()V");
			
			for (CompileMapper.Entry<ASMContext> entry: mappings.values()) {
				ASMContext c = entry.getCustom();
				argsList.add(c.callTarget);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitVarInsn(ALOAD, 1);
				if (c.argIndex <= 5)
					mv.visitInsn(ICONST_0 + c.argIndex);
				else
					mv.visitIntInsn(BIPUSH, c.argIndex);
				
				mv.visitInsn(AALOAD);
				mv.visitTypeInsn(CHECKCAST, c.argType);
				mv.visitFieldInsn(PUTFIELD, compiledClassName, c.argName, c.argDesc);
			}
			mv.visitInsn(RETURN);
			Label lLast = new Label();
			mv.visitLabel(lLast);
			mv.visitLocalVariable("this", thisValueType, null, lFirst, lLast, 0);
			mv.visitMaxs(3, 2);
			mv.visitEnd();
		}
		{
			mv = cw.visitMethod(ACC_PUBLIC, "size", "()I", null, null);
			mv.visitCode();
			Label l0 = new Label();
			mv.visitLabel(l0);
			mv.visitInsn(ICONST_3);
			mv.visitInsn(IRETURN);
			Label l1 = new Label();
			mv.visitLabel(l1);
			mv.visitLocalVariable("this", thisValueType, null, l0, l1, 0);
			mv.visitMaxs(1, 1);
			mv.visitEnd();
		}
		{
			mv = cw.visitMethod(ACC_PUBLIC, 
					resultType.getAdapterMethodName(), //"getInteger", 
					"(I)" + Type.getDescriptor(resultType.primitiveType()), //"(I)I" 
					null, null);
			mv.visitCode();

			final int viGetterIndex = 1;
			
			final Label lFirst = new Label();
			mv.visitLabel(lFirst);
			
			int idxVariable = viGetterIndex + 1;
			ASMContext prevContext = null;
			for (CompileMapper.Entry<ASMContext> entry: mappings.values()) {
				ASMContext pR = entry.getCustom();
				prevContext = pR;
				Label lN = new Label();
				pR.valuePos = lN;
				pR.valueIndex = idxVariable;
				
				mv.visitLabel(lN);
				FunctorMethodKind mk = pR.fm.kind();
				if (mk != FunctorMethodKind.CAST) {
					mv.visitVarInsn(ALOAD, 0);
					mv.visitFieldInsn(GETFIELD, compiledClassName, pR.argName, pR.argDesc);
				}				
				
				switch (mk) {
					case ARRAY_GET:
						mv.visitVarInsn(ILOAD, viGetterIndex);
						mv.visitInsn(switchOf(pR.valueVType, IALOAD, LALOAD, DALOAD));
						break;
					case INDEX_GET:
						mv.visitVarInsn(ILOAD, viGetterIndex);
						mv.visitMethodInsn(pR.targetType.isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL,
							pR.argType, pR.fm.name(), "(I)" + pR.valueDesc);
						break;
					case CAST: {
						ASMContext p0 = mappings.get(entry.adapter.getParam(0)).getCustom();
//						p0.valueLastUse 
						mv.visitVarInsn(switchOf(p0.valueVType, ILOAD, LLOAD, DLOAD), p0.valueIndex);
						mv.visitInsn(switchCast(p0.valueVType, pR.valueVType));
						break;
					}
					case VIRTUAL: {
						final NumericAdapter a = entry.adapter;
						StringBuilder paramsDesc = new StringBuilder();
						paramsDesc.append('(');
						int idxParam = -1;
						while (true) {
							idxParam++;
							NumericAdapter pNa = a.getParam(idxParam);
							if (pNa == null) break;
							ASMContext pN = mappings.get(pNa).getCustom();
							mv.visitVarInsn(switchOf(pN.valueVType, ILOAD, LLOAD, DLOAD), pN.valueIndex);
							ValueType expectedParamType = pR.fm.param(idxParam);
							if (expectedParamType != ValueType.UNKNOWN)
								mv.visitInsn(switchCast(pN.valueVType, expectedParamType));
							else 
								expectedParamType = pN.valueVType;
							paramsDesc.append(Type.getDescriptor(expectedParamType.primitiveType()));
						}
						paramsDesc.append(')');
						paramsDesc.append(pR.valueDesc);
						String methodName = pR.fm.name();
						mv.visitMethodInsn(pR.targetType.isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL,
								pR.argType, methodName, paramsDesc.toString());
						break;
					}
//					case STATIC:;
					default:
						throw new UnsupportedOperationException("Unsupported method kind: " + pR.fm.kind());
				}
				mv.visitVarInsn(switchOf(pR.valueVType, ISTORE, LSTORE, DSTORE), pR.valueIndex);
				idxVariable += switchOf(pR.valueVType, 1, 2, 2);
			}
			
//			mv.visitInsn(switchOf(prevContext.valueVType, ICONST_0, LCONST_0, DCONST_0));
			mv.visitVarInsn(switchOf(prevContext.valueVType, ILOAD, LLOAD, DLOAD), prevContext.valueIndex);
			mv.visitInsn(switchOf(prevContext.valueVType, IRETURN, LRETURN, DRETURN));
			
			Label lLast = new Label();
			prevContext.valueLastUse = lLast; 
			mv.visitLabel(lLast);
			mv.visitLocalVariable("this", thisValueType, null, lFirst, lLast, 0);
			mv.visitLocalVariable("index", "I", null, lFirst, lLast, viGetterIndex);
			for (CompileMapper.Entry<ASMContext> entry: mappings.values()) {
				ASMContext c = entry.getCustom();
				mv.visitLocalVariable(c.valueName, c.valueDesc, null, 
					c.valuePos, c.valueLastUse == null ? lLast : c.valueLastUse, c.valueIndex);
			}
			mv.visitMaxs(5, idxVariable);
			mv.visitEnd();
		}
		cw.visitEnd();

		return cw.toByteArray();
	}
}


class ASMContext {
	public final int argIndex;
	public final String argName;
	public final String argType;
	public final String argDesc;
	public final String valueName;
	public final ValueType valueVType;
	public final String valueType;
	public final String valueDesc;
	public final Object callTarget;
	public final Class<?> targetType;
	public final FunctorMethod fm; 
	public Label valuePos;
	public Label valueLastUse;
	public int valueIndex;
	
	public ASMContext(CompileMapper.Entry<?> entry) {
		argIndex = entry.seqNo;
		argName = "arg" + argIndex;
		FunctorInstance fi = entry.getFunctorInstance();
		fm = fi.getMethodInfo();
		Object target = fi.getTargetObject(entry.adapter);
		callTarget = target;
		Class<?> targetClass = fm.target();
		if (targetClass == null) targetClass = target.getClass();
		if (targetClass.isAnonymousClass()) targetClass = targetClass.getSuperclass();
		targetType = targetClass;
		argType = Type.getInternalName(targetType);
		argDesc = Type.getDescriptor(targetType);
		valueVType = fm.result();
		valueName = "v" + entry.seqNo;
		valueType = Type.getInternalName(valueVType.primitiveType());
		valueDesc = Type.getDescriptor(valueVType.primitiveType());
	}
}
